#include <DNSServer.h>
#include <EEPROM.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include "WiFi.h"

#define STO_IN_EEPROM_BASE_ADDR      0 //(4000)
#define STO_IN_EEPROM_DATA_MAX_SIZE  (1024)


typedef struct {
    int  isConfiged;
    char wifiSSID[64+1];
    char wifiPwd[64+1];
}WIFI_CONFIG;
typedef struct {
    char xzAuthCode[64+1];
}XZAUTH_CONFIG;
typedef struct {
    char calendarInfo[64+1];
}CALENDAR_CONFIG;

typedef struct {
    int             isBaseConfiged;
#define SCREEN_CYCLE_MIN_MINS  ( 1 )
#define SCREEN_CYCLE_MAX_MINS  ( 12*60 )
    uint32_t        refreshScreenCycleMins;      //刷屏周期最小1min最大60*12mins(12小时)
#define WIFI_STO_MAX_NUM  ( 3 )
    WIFI_CONFIG     wifi[WIFI_STO_MAX_NUM];     //N*(128+2)
    XZAUTH_CONFIG   xzAuth;    //(64+1)
    int             isCalendarConfiged;
#define CALENDAR_MAX_NUM  ( 3 )
    CALENDAR_CONFIG calendar[CALENDAR_MAX_NUM];  //N*(64+1)
}STO_CONFIG;
const IPAddress apIP(192, 168, 10, 1);
const IPAddress gateway(192, 168, 10, 1);  //网关地址
const IPAddress invalideIP(0, 0, 0, 0);
const byte DNS_PORT = 53; //DNS服务端口号，一般为53
const char* apSSID = "CFG-EPD-WiFi";
const char* apPWD  = "12345678";
boolean   settingMode;
String    ssidList;
String    xzAuthCode = "SP8CQ7SccstexxdLH"; //心知天气密钥
String    cnSSID;     //当前链接的WIFI-ssid
String    calendarInfo[CALENDAR_MAX_NUM];
uint32_t  refreshScreenCycleMins=5;       //刷屏周期默认5min
boolean   onSettingCalendarInfo = false; //配置完成, 确保延时休眠, 防止继续配置
DNSServer dnsServer;
WebServer webServer(80);

String getXZAuthCode()
{
    return xzAuthCode;
}

String getCalendarInfo(int index)
{
    if(index <0 || index >CALENDAR_MAX_NUM-1) return "";
    return calendarInfo[index];
}

uint32_t getScreenCycleMins(void)
{
    return (refreshScreenCycleMins < SCREEN_CYCLE_MIN_MINS) ? SCREEN_CYCLE_MIN_MINS : refreshScreenCycleMins > SCREEN_CYCLE_MAX_MINS ? SCREEN_CYCLE_MAX_MINS : refreshScreenCycleMins;;
}

boolean defaultConfig() {
  Serial.println("Try default Config...");
  String ssid = "TP-LINK_xxx";
  String pass = "12345678";
  String authCode= "cccCQ7SKasteDddLH";

  Serial.print("WiFi: ");
  Serial.println(ssid);
  xzAuthCode = authCode;
  cnSSID     = ssid;
  WiFi.begin(ssid.c_str(), pass.c_str());
  return true;
}
boolean secondConfig() {
  Serial.println("Try second Config...");
  String ssid = "Xiaomi_123";
  String pass = "12345678";
  String authCode= "SP8CQ7SccstexxdLH";
  Serial.println(ssid);
  xzAuthCode = authCode;
  cnSSID     = ssid;
  WiFi.begin(ssid.c_str(), pass.c_str());

  return true;
}

void refreshWifiConfig(WIFI_CONFIG newWifi, WIFI_CONFIG *stoWifi)
{
    int index=0;
    if(newWifi.isConfiged != 1 || stoWifi == NULL) return;

    for(index=0; index<WIFI_STO_MAX_NUM; index++) {
        if((stoWifi[index].isConfiged == 1) && (strcmp(newWifi.wifiSSID, stoWifi[index].wifiSSID) == 0))
        {
            break; //get pos index
        }
    }

    if(index == WIFI_STO_MAX_NUM) //新纪录
    {
        index = WIFI_STO_MAX_NUM-1; //从头更新到最后一个
    }
    else //将找到的SSID放到最前面, 然后依次排队, 更新到该位置
    {
        //int pos = index;
    }
    for(; index>0; index--) {
        stoWifi[index] = stoWifi[index-1];
    }
    stoWifi[index] = newWifi;
}

boolean restoreConfig(int maxNums, WIFI_CONFIG wifiTable[WIFI_STO_MAX_NUM]) {
  Serial.println("Reading EEPROM...");
  String ssid = "";
  String pass = "";
  String authcode= "";
  STO_CONFIG cfg;
  cfg.isBaseConfiged      = 0;
  cfg.isCalendarConfiged = 0;
  EEPROM.begin(STO_IN_EEPROM_DATA_MAX_SIZE);
  size_t rlen = EEPROM.readBytes(STO_IN_EEPROM_BASE_ADDR, &cfg, sizeof(cfg));
  EEPROM.end();
  if(rlen >0 && cfg.isCalendarConfiged == 1){
    refreshScreenCycleMins = cfg.refreshScreenCycleMins;
    Serial.println("刷屏周期： "+String(refreshScreenCycleMins)+" 分钟");
    for(int index=0; index<CALENDAR_MAX_NUM; index++)
    {
        calendarInfo[index] = cfg.calendar[index].calendarInfo;
        Serial.println("日程"+String(index+1)+"： "+calendarInfo[index]);
    }
  }
  else {
    Serial.println("Calendar config not found.");
  }
  Serial.println("");
  if(rlen >0 && cfg.isBaseConfiged == 1){
    authcode = cfg.xzAuth.xzAuthCode;
    Serial.print("密钥: ");
    Serial.println(authcode);

    xzAuthCode=authcode.c_str();
    for(int i=0; i<WIFI_STO_MAX_NUM, i<maxNums; i++) {
        wifiTable[i] = cfg.wifi[i];
    }
    return true;
  }
  else {
    Serial.println("Auth config not found.");
    return false;
  }
}

boolean tryConnection(int index, WIFI_CONFIG wifi) {
  Serial.printf("Try config %d connection...\r\n", index);
  String ssid = "";
  String pass = "";

  if(wifi.isConfiged == 1){
    ssid = wifi.wifiSSID;
    Serial.print("WiFi: ");
    Serial.println(ssid);

    pass = wifi.wifiPwd;
    Serial.print("密码: ");
    Serial.println(pass);

    cnSSID    = ssid;
    WiFi.begin(ssid.c_str(), pass.c_str());
    return true;
  }
  else {
    Serial.printf("Wifi config %d not found.\r\n", index);
    return false;
  }
}

boolean checkConnection() {
  int count = 0;
  Serial.println("正在等待连接");
  while ( count < 24 ) {
    if (WiFi.status() == WL_CONNECTED) {
      count = 0;
      while ( count < 10 ) {
          if(WiFi.localIP().toString() != invalideIP.toString()) break;
          delay(500);
          Serial.print("+");
          count++;
      }
      Serial.println("成功连接！");
      Serial.print("Local IP: ");
      Serial.println(WiFi.localIP());
      return (true);
    }
    delay(500);
    Serial.print(".");
    count++;
  }
  Serial.println("连接超时.");
  return false;
}

String makePage(String title, String contents) {
  String s = "<!DOCTYPE html><html><head>";
  s += "<meta name=\"viewport\" content=\"width=device-width,user-scalable=0\">";
  s += "<title>";
  s += title;
  s += "</title></head><body>";
  s += contents;
  s += "</body></html>";
  return s;
}

String urlDecode(String input) {
  String s = input;
  s.replace("%20", " ");
  s.replace("+", " ");
  s.replace("%21", "!");
  s.replace("%22", "\"");
  s.replace("%23", "#");
  s.replace("%24", "$");
  s.replace("%25", "%");
  s.replace("%26", "&");
  s.replace("%27", "\'");
  s.replace("%28", "(");
  s.replace("%29", ")");
  s.replace("%30", "*");
  s.replace("%31", "+");
  s.replace("%2C", ",");
  s.replace("%2E", ".");
  s.replace("%2F", "/");
  s.replace("%2C", ",");
  s.replace("%3A", ":");
  s.replace("%3A", ";");
  s.replace("%3C", "<");
  s.replace("%3D", "=");
  s.replace("%3E", ">");
  s.replace("%3F", "?");
  s.replace("%40", "@");
  s.replace("%5B", "[");
  s.replace("%5C", "\\");
  s.replace("%5D", "]");
  s.replace("%5E", "^");
  s.replace("%5F", "-");
  s.replace("%60", "`");
  return s;
}

void OnWebServer(boolean firstsettingMode) {
  Serial.println("On web server "+String(firstsettingMode));
  if (firstsettingMode) {
    webServer.on("/settings", []() {
      String s = "<head><meta charset=\"utf-8\"></head><h1>EPD-Wi-Fi配置</h1><p>请在选择WiFi名称后输入对应的WiFi密码.</p>";
      s += "<form method=\"get\" action=\"setap\"><label>网络:</label><select name=\"ssid\">";
      s += ssidList;
      s += "</select><br>密码:<input name=\"pass\" length=64 type=\"password\"><br>心知密钥:<input name=\"authcode\" length=64 type=\"password\"><br>";
      s += "<input name=\"保存并提交\"  type=\"submit\"></form>";
      webServer.send(200, "text/html", makePage("EPD-Wi-Fi配置", s));
    });
    webServer.on("/setap", []() {
      String ssid = urlDecode(webServer.arg("ssid"));
      Serial.print("SSID: ");
      Serial.println(ssid);
      String pass = urlDecode(webServer.arg("pass"));
      Serial.print("Password: ");
      Serial.println(pass);
      String authcode = urlDecode(webServer.arg("authcode"));
      Serial.print("authcode: ");
      Serial.println(authcode);

      STO_CONFIG  cfg;
      WIFI_CONFIG wifi = {0};
      EEPROM.begin(STO_IN_EEPROM_DATA_MAX_SIZE);
      EEPROM.readBytes(STO_IN_EEPROM_BASE_ADDR, &cfg, sizeof(cfg));
      Serial.println("Read EEPROM isConfiged: " + String(cfg.isBaseConfiged) + " WIFI SSID: " + String(cfg.wifi[0].wifiSSID));

      for (int i = 0; i < ssid.length(),i<sizeof(wifi.wifiSSID)-1; ++i) {
        wifi.wifiSSID[i] = ssid[i];
        wifi.isConfiged = 1;
      }
      for (int i = 0; i < pass.length(), i<sizeof(wifi.wifiPwd)-1; ++i) {
        wifi.wifiPwd[i] = pass[i];
      }
      for (int i = 0; i < authcode.length(), i<sizeof(cfg.xzAuth.xzAuthCode)-1; ++i) {
        cfg.xzAuth.xzAuthCode[i] = authcode[i];
      }
      //循环存储WIFI信息
      refreshWifiConfig(wifi, cfg.wifi);
      cfg.isBaseConfiged = 1;
      Serial.println("Writing ssid password authcode to EEPROM...");
      EEPROM.writeBytes(STO_IN_EEPROM_BASE_ADDR, &cfg, sizeof(cfg));
      EEPROM.commit();
      EEPROM.end();
      Serial.println("Write EEPROM done!");
      String s = "<head><meta charset=\"utf-8\"><h1>设置结束！</h1><p>设备即将在重启后接入 \"";

      s += ssid;
      s += "\" ";
      webServer.send(200, "text/html", makePage("EPD-Wi-Fi配置", s));
      delay(300);
      ESP.restart();
    });
    webServer.onNotFound([]() {
      String s = "<head><meta charset=\"utf-8\"><h1>配网模式</h1><p><a href=\"/settings\">点击配网</a></p>";
      webServer.send(200, "text/html", makePage("配网模式", s));
    });
  }
  else {
    onSettingCalendarInfo = true;
    webServer.on("/", []() {
      String s = "<head><meta charset=\"utf-8\"></head><h1>STA mode</h1><p>"\
                  "<a href=\"/reset\">重置WiFi设置</a><br>"\
                  "<a href=\"/refresh\">刷屏周期设置</a><br>"\
                  "<a href=\"/calendar\">日程配置</a></p>";
      webServer.send(200, "text/html", makePage("STA mode", s));
    });
    onSettingCalendarInfo = true;
    webServer.on("/calendar", []() {
      String s = "<head><meta charset=\"utf-8\"></head><h1>日程配置</h1><p>请配置日程信息：</p>";
      s += "<form method=\"get\" action=\"save-calendar\"><br>";
      s += "日程1：<input name=\"calendar_1\" value=\"";
      s += calendarInfo[0];
      s += "\" length=32 type=\"text\"><br>";
      s += "日程2：<input name=\"calendar_2\" value=\"";
      s += calendarInfo[1];
      s += "\"length=32 type=\"text\"><br>";
      s +=  "日程3：<input name=\"calendar_3\" value=\"";
      s += calendarInfo[2];
      s += "\"length=32 type=\"text\"><br>";
      s += "<input name=\"保存并提交\"  type=\"submit\">";
      s += "</form>";
      webServer.send(200, "text/html", makePage("日程配置", s));
    });

    webServer.on("/save-calendar", []() {
      String cal;
      STO_CONFIG cfgOld;
      STO_CONFIG cfg;
      EEPROM.begin(STO_IN_EEPROM_DATA_MAX_SIZE);
      size_t rlen = EEPROM.readBytes(STO_IN_EEPROM_BASE_ADDR, &cfgOld, sizeof(cfgOld));
      cfg = cfgOld;
      if(rlen >0 && cfgOld.isCalendarConfiged == 1){
          for(int index=0; index<CALENDAR_MAX_NUM; index++)
          {
              Serial.println("旧日程"+String(index+1)+"： "+String(cfgOld.calendar[index].calendarInfo));
          }
      }
      Serial.println("");
      for(int index=0; index<CALENDAR_MAX_NUM; index++)
      {
          cal = urlDecode(webServer.arg("calendar_"+String(index+1)));
          calendarInfo[index] = cal;
          for (int i = 0; i < cal.length(),i<sizeof(cfg.calendar[index].calendarInfo)-1; ++i) {
            cfg.calendar[index].calendarInfo[i] = cal[i];
          }
          Serial.println("新日程"+String(index+1)+"： "+calendarInfo[index]);
      }

      cfg.isCalendarConfiged = 1;
      if(memcmp_P(&cfg, &cfgOld, sizeof(cfgOld)) != 0)
      {
          Serial.println("Writing calendar info to EEPROM...");
          EEPROM.writeBytes(STO_IN_EEPROM_BASE_ADDR, &cfg, sizeof(cfg));
          EEPROM.commit();
          Serial.println("Write EEPROM done!");
      }
      else
      {
          Serial.println("Calendar is same so does not write to EEPROM!");
      }
      EEPROM.end();

      String s = "<head><meta charset=\"utf-8\"></head><h1>日程已保存（下次屏幕刷新呈现）</h1><p><a href=\"/calendar\">返回日程配置</a></p>";
      webServer.send(200, "text/html", makePage("日程配置", s));
      onSettingCalendarInfo = true; //配置完成, 确保延时休眠, 防止继续配置
    });

    webServer.on("/refresh", []() {
      String s = "<head><meta charset=\"utf-8\"></head><h1>屏幕更新周期配置</h1><p>请配置刷新周期（单位：分钟）：</p>";
             s += "<form method=\"get\" action=\"save-screenCycle\"><br>";
             s += "更新周期：<input name=\"screenCycle\" value=\"";
             s += refreshScreenCycleMins;
             s += "\" length=4 type=\"text\"><br>";
             s += "<input name=\"保存并提交\"  type=\"submit\">";
             s += "</form>";
      webServer.send(200, "text/html", makePage("屏幕更新周期配置", s));
    });

    webServer.on("/save-screenCycle", []() {
      String screenCycle;
      STO_CONFIG cfgOld;
      STO_CONFIG cfg;
      EEPROM.begin(STO_IN_EEPROM_DATA_MAX_SIZE);
      size_t rlen = EEPROM.readBytes(STO_IN_EEPROM_BASE_ADDR, &cfgOld, sizeof(cfgOld));
      cfg = cfgOld;
      if(rlen >0 && cfgOld.isCalendarConfiged == 1){
          Serial.println("旧刷屏周期： "+String(cfgOld.refreshScreenCycleMins)+" 分钟");
      }
      Serial.println("");
      screenCycle = urlDecode(webServer.arg("screenCycle"));
      int cycle = atoi(screenCycle.c_str());
      refreshScreenCycleMins = cycle < SCREEN_CYCLE_MIN_MINS ? SCREEN_CYCLE_MIN_MINS : cycle > SCREEN_CYCLE_MAX_MINS ? SCREEN_CYCLE_MAX_MINS : cycle;
      cfg.refreshScreenCycleMins = refreshScreenCycleMins;
      Serial.println("新刷屏周期："+String(refreshScreenCycleMins)+" 分钟");

      if(memcmp_P(&cfg, &cfgOld, sizeof(cfgOld)) != 0)
      {
          Serial.println("Writing screen upload cycle info to EEPROM...");
          EEPROM.writeBytes(STO_IN_EEPROM_BASE_ADDR, &cfg, sizeof(cfg));
          EEPROM.commit();
          Serial.println("Write EEPROM done!");
      }
      else
      {
          Serial.println("Calendar is same so does not write to EEPROM!");
      }
      EEPROM.end();

      String s = "<head><meta charset=\"utf-8\"></head><h1>屏幕更新周期已保存</h1><p><a href=\"/refresh\">返回屏幕更新周期配置</a></p>";
      webServer.send(200, "text/html", makePage("屏幕更新周期配置", s));
      onSettingCalendarInfo = true; //配置完成, 确保延时休眠, 防止继续配置
    });

    webServer.on("/reset", []() {
        STO_CONFIG cfg;
        EEPROM.begin(STO_IN_EEPROM_DATA_MAX_SIZE);
        EEPROM.readBytes(STO_IN_EEPROM_BASE_ADDR, &cfg, sizeof(cfg));
        Serial.println("Read EEPROM isConfiged: " + String(cfg.isBaseConfiged));
        if(cfg.isBaseConfiged != 0)
        {
            cfg.isBaseConfiged = 0;
            Serial.println("Clear ssid password authcode to EEPROM...");
            EEPROM.writeBytes(STO_IN_EEPROM_BASE_ADDR, &cfg, sizeof(cfg));
            EEPROM.commit();
            Serial.println("Clear EEPROM done!");
        }
        EEPROM.end();

        String s = "<head><meta charset=\"utf-8\"></head><h1>Wi-Fi 设置已重置</h1><p>请重启设备.</p>";
        webServer.send(200, "text/html", makePage("Reset Wi-Fi Settings", s));
    });
  }
  dnsServer.start(DNS_PORT, "config.com", WiFi.softAPIP());
  webServer.begin();
}

void startWebServer() {
//  display.drawInvertedBitmap(0, 0, jitang, 400, 300, GxEPD_BLACK);//画图
//  Serial.println("开屏动画");
  
  if (settingMode) {
    Serial.print("Starting Web AP Server at ");
    Serial.println(WiFi.softAPIP());
    OnWebServer(true);
  }
  else {
    Serial.print("Starting Web Local Server at ");
    Serial.println(WiFi.localIP());
    OnWebServer(false);
  }
}

void wifi_apcfg_notice_display()
{
  String ssidinfo = "AP 热点: " + String(apSSID);
  String pwdinfo  = "AP 密码: " + String(apPWD);
  String webinfo  = "WEB地址: " + /*apIP.toString()*/WiFi.softAPIP().toString();

  /*如果联网失败就显示失败提醒*/
  display.fillScreen(GxEPD_WHITE);
  extern U8G2_FOR_ADAFRUIT_GFX u8g2Fonts;
  u8g2Fonts.setFont(chinese_city_gb2312);
  u8g2Fonts.setForegroundColor(GxEPD_RED);  // 设置前景色
  u8g2Fonts.setBackgroundColor(GxEPD_WHITE);// 设置背景色
  u8g2Fonts.setCursor(40, 240);
  u8g2Fonts.print(ssidinfo);
  u8g2Fonts.setCursor(40, 260);
  u8g2Fonts.print(pwdinfo);
  u8g2Fonts.setCursor(40, 280);
  u8g2Fonts.print(webinfo);
  display.drawInvertedBitmap(0, 0, jitang, 400, 300, GxEPD_BLACK);//画图
  display.nextPage();
}

void setupMode() {
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);
  int n = WiFi.scanNetworks();
  delay(100);
  Serial.println("");
  for (int i = 0; i < n; ++i) {
    ssidList += "<option value=\"";
    ssidList += WiFi.SSID(i);
    ssidList += "\">";
    ssidList += WiFi.SSID(i);
    ssidList += "</option>";
  }
  uint32_t updateTick = 0;
  delay(100); updateTick+=100;
  WiFi.disconnect();

  while(updateTick <= 30000)
  {
    while( WiFi.mode(WIFI_AP) != true)
    {
      delay(500); updateTick+=500;
    }
    WiFi.softAPConfig(apIP, gateway, IPAddress(255, 255, 255, 0));
    WiFi.softAP(apSSID, apPWD, 13);
    delay(200); updateTick+=200;
    if(apIP == WiFi.softAPIP()) break;
    delay(500); updateTick+=500;
    Serial.print("Wifi AP IP:"+WiFi.softAPIP().toString());
    Serial.println("does not setted, reconfig now!");
  }
  delay(200); 
  startWebServer();
  Serial.print("Starting Access Point at \"");
  Serial.print(apSSID);
  Serial.println("\"");
  delay(1000);
  wifi_apcfg_notice_display();
}

void wifi_init(){
  delay(10);
  WIFI_CONFIG wifi[WIFI_STO_MAX_NUM] = {0};
  if (restoreConfig(WIFI_STO_MAX_NUM, wifi)) {
    for(int i=0; i<WIFI_STO_MAX_NUM; i++) {
      if (tryConnection(i+1, wifi[i])) {
        if (checkConnection()) {
          //循环存储WIFI信息
          STO_CONFIG cfg;
		  STO_CONFIG cfgOld;
		  EEPROM.begin(STO_IN_EEPROM_DATA_MAX_SIZE);
		  EEPROM.readBytes(STO_IN_EEPROM_BASE_ADDR, &cfgOld, sizeof(cfgOld));
		  cfg = cfgOld;

          refreshWifiConfig(wifi[i], cfg.wifi);
		  if(memcmp_P(&cfg, &cfgOld, sizeof(cfgOld)) != 0)
		  {
			  Serial.println("Refres wifi info on frist to EEPROM...");
			  EEPROM.writeBytes(STO_IN_EEPROM_BASE_ADDR, &cfg, sizeof(cfg));
			  EEPROM.commit();
			  Serial.println("Write EEPROM done!");
		  }
		  else
		  {
			  Serial.println("Refres wifi info is same so does not write to EEPROM!");
          }
          EEPROM.end();

          settingMode = false;
          startWebServer();
          return;
        }
      }
    }
  }
#if 1
  else
  {
    defaultConfig();
    if (checkConnection()) {
      settingMode = false;
      startWebServer();
      return;
    }
    secondConfig();
    if (checkConnection()) {
      settingMode = false;
      startWebServer();
      return;
    }
  }
#endif
  settingMode = true;
  setupMode();
}
